<html>
  <head>
    <title>Variables</title>
    <script type="text/javascript" src="protovis-d3.2.js"></script>
    <script type="text/javascript" src="../visualization/jquery/jquery-1.4.2.js"></script>
    <!-- data.js defines a variable 'data', a rectangular array of text strings (some of which may represent numbers). 
         We expect the first row to contain column labels. 
         studentMCAS.js defines studentMCAS, a similar array containing student MCAS histories per subject.
         In this array numbers are already numbers and missing values are already nulls.
    -->
    <script type="text/javascript" src="file://C:/Documents and Settings/dolsson/My Documents/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/data.js"></script>
    <script type="text/javascript" src="file://C:/Documents and Settings/dolsson/My Documents/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/student MCAS.js"></script>
    <style type="text/css">
        .header-left { text-align: left }
        .padleft { padding-left: 10px }
        #variablesDiv { font-family: sans-serif; margin-top: 0.25in }
        .checkboxColumn { text-align: center;  }
    </style>
  </head>
  <body>
    <div id="parallelCoordDiv">
        <script type="text/javascript">

var vis;  // will hold the root panel of the visualization
var selected;
// Sizing
var wOverall = screen.availWidth * 0.9, 
    hOverall = screen.availHeight * 0.8;
var padding = { left: 30, right: 30, top: 30, bottom: 20 };
var w = wOverall - padding.left - padding.right,
    h = hOverall - padding.top - padding.bottom;
$('#parallelCoordDiv').hide().width(wOverall).height(hOverall);

// Let's figure our what variables we have.
// Create a list of dictionaries, one for each variable.
// The list will remain in the same order as the data columns we received.
var vars = data.shift().map( function(label, col) { return { label: label, position: col } } );
var mcasVars = studentMCAS.shift().map( function(label, col) { return { label: label, position: col } } );
var displayCols = [];  // column indices to display in visualization
var filter = [];  // filters per column
var textSampleSize = 5;  // number of text values to display as sample

// Go ahead and convert numbers to stored numbers, empty strings to nulls.
// Special case: strings of digits (with no decimal point) longer than 5 characters are kept as strings.
data.forEach( function(row) { row.forEach( function(val, i, row) 
            { if (val == '') row[i] = null; else if (val.length < 6 && !isNaN(Number(val))) row[i] = Number(val) } ) } );

// Analyze the values in each column.
vars.forEach(analyzeColumn);
// Guess variable type: empty; identifier; grade; category; score (numeric); rating (text).
vars.forEach(classifyVariable);
// Create vertical scale functions and visibility functions for each variable.
vars.forEach(mkScale);
vars.forEach(mkVisibility);
// Display variables.
$(displayVariables);  // when DOM is ready

function analyzeColumn(thisVar, i) {
    thisVar.uniqCount = 0;
    thisVar.hasEmpty = false;
    thisVar.uniqNumCount = 0;
    thisVar.uniqTextCount = 0;
    thisVar.min = null;
    thisVar.max = null;
    thisVar.textSample = [];
    var u = pv.uniq(data, function(row) { return row[i] } );
    u.forEach(analyzeUniq, thisVar);
    thisVar.mostlyNum = thisVar.uniqNumCount > thisVar.uniqTextCount * 4;
}

function analyzeUniq(val) {
    this.uniqCount++;
    if (val === null)
        this.hasEmpty = true;
    else if (typeof val == 'string') {
        this.uniqTextCount++;
        if (this.textSample.length < textSampleSize)
            this.textSample.push(val);
    }
    else {
        this.uniqNumCount++;
        if (this.min === null || val < this.min)
            this.min = val;
        if (this.max === null || val > this.max)
            this.max = val;
    }
}

function classifyVariable(thisVar) {
    var SasidExp = /sasid/i;
    var IdExp = /lasid/i;
    var ScaledScoreExp = /scaled.score|mcas.ela|mcas.math|mcas.ste/i;
    var GradeExp = /grade/i;
    if (thisVar.uniqCount == 1 && thisVar.hasEmpty) thisVar.type = 'empty';
    else if (SasidExp.test(thisVar.label)) thisVar.type = 'SASID';
    else if (IdExp.test(thisVar.label)) thisVar.type = 'identifier';
    else if (GradeExp.test(thisVar.label)) thisVar.type = 'grade level';
    else if (thisVar.uniqCount < 4) thisVar.type = 'category';
    else if (thisVar.mostlyNum) {
        if (ScaledScoreExp.test(thisVar.label))
            thisVar.type = 'MCAS scaled score';
        else
            thisVar.type = 'score';
    }
    else if (thisVar.uniqCount > 0.8 * data.length) thisVar.type = 'identifier';
    else thisVar.type = 'category';
}

function mkScale(v) {
    // Make a vertical scaling function for this variable.
    switch (v.type) {
        case 'empty':
            v.scale = null;
            break;
        case 'SASID':
        case 'identifier':
        case 'category':
            v.scale = pv.Scale.ordinal(pv.uniq(data, function(row) { return row[v.position] }).sort()).split(0, h);
            break;
        case 'grade level':
            v.scale = pv.Scale.ordinal(pv.uniq(data, function(row) { return row[v.position] }).sort(numOrder)).split(0, h);
            break;
        case 'score':
            v.scale = pv.Scale.linear(v.min, v.max).range(0, h);
            break;
        case 'MCAS scaled score':
            v.scale = pv.Scale.linear(200, 280).range(0, h);
            break;
    }
}

function mkVisibility(v) {
    v.visMin = null;
    v.visMax = null;
    v.visible = isVis;
}

function isVis(val) {
    if (this.visMin === null)
        return true;
    var y = this.scale(val); 
    return y >= this.visMin && y <= this.visMax ;
}

function displayVariables() {
    $('#variablesDiv').append('<form><table id="variablesTable"><tr>' +
            '<th class="header-left">variable</th>' +
            '<th class="checkboxColumn">show</th>' +
            '<th>type</th><th>filter</th>' +
            '<th class="padleft header-left">range</th>' +
            '</tr></table></form>');
    vars.forEach(displayVar);
}

function displayVar(v) {
    var tr = $('<tr><td class="varName">' + safe(v.label) + '</td></tr>');
    tr.append('<td class="checkboxColumn">' + '<input type="checkbox" name="column' + v.position + 'Show" onchange="showVar(this)" value="off">' + '</td>');
    tr.append('<td>' + varTypeSelectControl(v) + '</td>');
    tr.append('<td>' + varValueSelectControl(v) + '</td>');
    var text = '';
    if (v.mostlyNum)
        text += v.min + ' - ' + v.max;
    if (v.uniqTextCount) {
        if (text.length > 1) text += ', ';
        text += v.textSample.join(', ');
        if (v.uniqTextCount > v.textSample.length) text += ',...';
    }
    tr.append('<td class="padleft">' + text + '</td>');
    $('#variablesTable').append(tr);
}

function varTypeSelectControl(v) {
    var html = '<select name="column' + v.position + 'TypeSelect" onchange="selectType(this)">';
    html += ['empty','identifier','SASID','grade level','category','score','MCAS scaled score'].map(
            function(t) { return '<option' + (t == v.type ? ' selected' : '') + '>' + t + '</option>' } );
    html += '</select>';
    return html;
}

function varValueSelectControl(v) {
    var html = '<select multiple name="column' + v.position + 'ValueSelect" onchange="includeValues(this)">';
    html += '</select>';
    return '';
}

function colPosition(vars, label) {
    var v;
    for (v in vars)
        if (vars[v].label == label)
            return vars[v].position;
    return null;
}

function numOrder(a, b) { return a - b; }  // for numeric sorting

function safe(s) {
    // Return an HTML-safe string with character entities.
    s = s.replace(/&/g, '&amp;');
    s = s.replace(/</g, '&lt;');
    s = s.replace(/>/g, '&gt;');
    return s;
}

function showVar(checkbox) {
    // This 'show' checkbox input element's value has changed.
    var nameExp = /column(\d+)Show/;
    var match = nameExp.exec(checkbox.name);
    var column = Number(match[1]);
    if (checkbox.checked)
        displayCols.push(column);
    else
        displayCols.splice(displayCols.indexOf(column), 1);
    drawParallelCoords();
}

function visible(row) {
    for (iDisp = 0; iDisp < displayCols.length; iDisp++) {
        var col = displayCols[iDisp];
        if (!vars[col].visible(row[vars[col].position]))
            return false;
    }
    return true;
}

function drawParallelCoords() {
    var x = pv.Scale.ordinal(displayCols).split(0, w);
    vis = new pv.Panel()
        .canvas('parallelCoordDiv')
        .width(w)
        .height(h)
        .left(padding.left)
        .right(padding.right)
        .top(padding.top)
        .bottom(padding.bottom)
        .fillStyle('lightgray')
        ;
    vis.add(pv.Panel)  // all occurrences
        .data(data)
        .add(pv.Line)
        .data(displayCols)
        .left(x)
        .bottom( function(col, row) { return vars[col].scale(row[col]) } )
        .strokeStyle('gray')
        .add(pv.Dot)
        .fillStyle('aliceblue')
        .title( function(col, row) { return row[col] } )
        ;
    vis.add(pv.Label)  // axis labels
        .data(displayCols)
        .bottom(-5)
        .textBaseline('top')
        .left(x)
        .textAlign('center')
        .font("12px sans-serif")
        .text( function(col) { return vars[col].label } )
        .fillStyle('lightgray')
        .events('all')
        .event('click', deselect)
        ;
    vis.add(pv.Panel)  // axis value labels for categories and grades
        .data(displayCols)
        .left(x)
        .visible( function(col) { return ['category','grade level'].indexOf(vars[col].type) >= 0 } )
        .add(pv.Label)
        .data( function(col) { return vars[col].scale.domain() } )
        .bottom( function(val, col) { return vars[col].scale(val) } )
        .font("12px sans-serif")
        .text( function(label) { return label ? label : '(none)' } )
        ;
    vis.add(pv.Panel)  // axis value labels for scores
        .data(displayCols)
        .left(x)
        .visible( function(col) { return ['score','MCAS scaled score'].indexOf(vars[col].type) >= 0 } )
        .add(pv.Label)
        .data( function(col) { return vars[col].scale.ticks() } )
        .bottom( function(val, col) { return vars[col].scale(val) } )
        .font("12px sans-serif")
        .text( function(val, col) { return vars[col].scale.tickFormat(val) } )
        ;
    var selectZone = vis.add(pv.Panel)
        .data(displayCols.map(function(col) { return {y:0, dy:h, col:col}; }))
        .left(function(d) x(d.col) - 30)
        .width(20)
        .fillStyle("rgba(0,0,0,.001)")
        .cursor("crosshair")
        .event("mousedown", pv.Behavior.select())
        .event("select", update)
        .event("selectend", selectAll)
        ;
    vis.render();
    if ($('#parallelCoordDiv').css('display') == 'none')
        $('#parallelCoordDiv').slideDown();

}
function drawSelected() {
    var x = pv.Scale.ordinal(displayCols).split(0, w);
    selected = vis.add(pv.Panel)  // selected occurrences
        .data(data)
        .visible( function(row) { return visible(row) } )
        .add(pv.Line)
        .data(displayCols)
        .left(x)
        .bottom( function(col, row) { return vars[col].scale(row[col]) } )
        .strokeStyle('blue')
        .add(pv.Dot)
        .fillStyle('lightblue')
        .title( function(col, row) { return row[col] } )
        ;
    selected.render();
}
function update(d) {
    console.log('update');
    vars[d.col].visMin = Math.max(0, h - d.y - d.dy);
    vars[d.col].visMax = Math.min(h, h - d.y);
    drawSelected();
}
function selectAll(d) {
    console.log(d.dy);
    if (d.dy != 0) {
        vars[d.col].visMin = Math.max(0, h - d.y - d.dy);
        vars[d.col].visMax = Math.min(h, h - d.y);
        drawSelected();
    } else {
        deselect(d.col);
    }
}

function deselect(col) {
    vars[col].minVis = null;
    vars[col].maxVis = null;
    drawSelected();
}

            </script>
        </div>
        <div id="variablesDiv">
        </div>
    </body>
</html>
